/*****************************************************************************
 * Copyright (c) 2021 Christian W. Damus, CEA LIST, and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Christian W. Damus - Initial API and implementation
 *
 *****************************************************************************/

package org.eclipse.papyrus.toolsmiths.validation.properties.internal.util;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcoreFactory;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.papyrus.infra.constraints.environment.ConstraintType;
import org.eclipse.papyrus.infra.emf.utils.EMFHelper;
import org.eclipse.papyrus.infra.properties.contexts.Context;
import org.eclipse.papyrus.infra.properties.contexts.DataContextElement;
import org.eclipse.papyrus.infra.properties.contexts.DataContextPackage;
import org.eclipse.papyrus.infra.properties.contexts.Section;
import org.eclipse.papyrus.infra.properties.contexts.Tab;
import org.eclipse.papyrus.infra.properties.contexts.View;
import org.eclipse.papyrus.infra.properties.environment.Environment;
import org.eclipse.papyrus.toolsmiths.validation.properties.internal.trace.ComposedSourceTraceHelper;
import org.eclipse.papyrus.toolsmiths.validation.properties.internal.trace.SourceTraceHelper;
import org.eclipse.uml2.common.util.CacheAdapter;

import com.google.common.collect.ImmutableList;

/**
 * Cache of derived information about <em>Properties View</em> models.
 */
public class PropertiesCache {

	static final PropertiesCache INSTANCE = new PropertiesCache();

	private final Map<?, ?> cacheMisses;
	private final Map<?, ?> defaults;

	private final Object contextKey = URI.createURI("PropertiesCache:context"); //$NON-NLS-1$
	private final Object sourceKey = URI.createURI("PropertiesCache:source"); //$NON-NLS-1$
	private final Object nameKey = URI.createURI("PropertiesCache:name"); //$NON-NLS-1$
	private final Object nestedPackagesKey = URI.createURI("PropertiesCache:nestedPackages"); //$NON-NLS-1$
	private final Object classesKey = URI.createURI("PropertiesCache:classes"); //$NON-NLS-1$
	private final Object propertiesKey = URI.createURI("PropertiesCache:properties"); //$NON-NLS-1$
	private final Object isPropertyRedefinitionKey = URI.createURI("PropertiesCache:isPropertyRedefinition"); //$NON-NLS-1$
	private final Object superclassesKey = URI.createURI("PropertiesCache:superclasses"); //$NON-NLS-1$
	private final Object viewsKey = URI.createURI("PropertiesCache:views"); //$NON-NLS-1$
	private final Object sectionsKey = URI.createURI("PropertiesCache:sections"); //$NON-NLS-1$
	private final Object constraintTypesKey = URI.createURI("constraintTypes"); //$NON-NLS-1$

	private final Object allDataContextElementsKey = new Object();

	private final SourceTraceHelper sourceTraceHelper = new ComposedSourceTraceHelper();

	private PropertiesCache() {
		super();

		EObject dummy = EcoreFactory.eINSTANCE.createEObject();

		cacheMisses = Map.of(sourceKey, dummy, classesKey, List.of(dummy), propertiesKey, List.of(dummy));

		// Don't need null defaults (e.g. for sourceKey) in the map
		defaults = Map.of(classesKey, List.of(), propertiesKey, List.of());
	}

	public static PropertiesCache getInstance(Notifier notifier) {
		return INSTANCE;
	}

	private static CacheAdapter getCacheAdapter(EObject object) {
		// As generated by the Properties model
		return CacheAdapter.getInstance();
	}

	public Context getContext(EObject contextElement) {
		return getOrCache(contextElement, contextKey, e -> {
			EObject root = EcoreUtil.getRootContainer(e);
			return root instanceof Context ? (Context) root : null;
		});
	}

	public EObject getSourceElement(EObject propertiesElement) {
		return getOrCache(propertiesElement, sourceKey, sourceTraceHelper::getSourceElement);
	}

	public String getName(EObject sourceElement) {
		return getOrCache(sourceElement, nameKey, sourceTraceHelper::getName);
	}

	public List<? extends EObject> getNestedPackages(EObject sourcePackage) {
		return getOrCache(sourcePackage, nestedPackagesKey, sourceTraceHelper::getNestedPackages);
	}

	public List<? extends EObject> getClasses(EObject sourcePackage) {
		return getOrCache(sourcePackage, classesKey, sourceTraceHelper::getClasses);
	}

	public List<DataContextElement> getDataContextElements(Context context) {
		return getOrCache(context, allDataContextElementsKey, this::getAllDataContextElements);
	}

	private List<DataContextElement> getAllDataContextElements(Context context) {
		ImmutableList.Builder<DataContextElement> result = ImmutableList.builder();
		ResourceSet rset = EMFHelper.getResourceSet(context);

		for (TreeIterator<?> contents = rset.getAllContents(); contents.hasNext();) {
			Object next = contents.next();
			if (next instanceof DataContextElement) {
				result.add((DataContextElement) next);
			} else if (next instanceof Tab || next instanceof View) {
				contents.prune();
			}
		}

		return result.build();
	}

	public List<View> getViews(DataContextElement element) {
		return getOrCache(element, viewsKey, this::getViewsOf);
	}

	private List<View> getViewsOf(DataContextElement element) {
		return allViews(getContext(element))
				.filter(view -> sourceTraceHelper.isViewOf(view, element))
				.collect(ImmutableList.toImmutableList());
	}

	private Stream<View> allViews(Context context) {
		return context.getViews().stream();
	}

	public List<Section> getSections(DataContextElement element) {
		return getOrCache(element, sectionsKey, this::getSectionsOf);
	}

	private List<Section> getSectionsOf(DataContextElement element) {
		return allSections(getContext(element))
				.filter(section -> sourceTraceHelper.isSectionFor(section, element))
				.collect(ImmutableList.toImmutableList());
	}

	private Stream<Section> allSections(Context context) {
		return context.getTabs().stream()
				.map(Tab::getSections).flatMap(Collection::stream);
	}

	public Optional<DataContextElement> getDataContextElement(DataContextPackage package_, EObject sourceClass) {
		Context context = getContext(package_);
		return getDataContextElements(context).stream()
				.filter(element -> getSourceElement(element) == sourceClass)
				.findAny();
	}

	public List<? extends EObject> getProperties(EObject sourceClass) {
		return getOrCache(sourceClass, propertiesKey, sourceTraceHelper::getProperties);
	}

	public boolean isPropertyRedefinition(EObject sourceProperty) {
		return getOrCache(sourceProperty, isPropertyRedefinitionKey, sourceTraceHelper::isPropertyRedefinition);
	}

	public List<? extends EObject> getSuperclasses(EObject sourceClass) {
		return getOrCache(sourceClass, superclassesKey, sourceTraceHelper::getSuperclasses);
	}

	public List<ConstraintType> getConstraintTypes(Context context) {
		return getOrCache(context, constraintTypesKey, this::getAllConstraintTypes);
	}

	private List<ConstraintType> getAllConstraintTypes(Context context) {
		ImmutableList.Builder<ConstraintType> result = ImmutableList.builder();

		ResourceSet rset = EMFHelper.getResourceSet(context);
		for (Resource resource : rset.getResources()) {
			for (EObject root : resource.getContents()) {
				if (root instanceof Environment) {
					result.addAll(((Environment) root).getConstraintTypes());
				}
			}
		}

		return result.build();
	}

	private <T extends EObject, V> V getOrCache(T object, Object key, Function<? super T, V> computer) {
		CacheAdapter cache = getCacheAdapter(object);

		@SuppressWarnings("unchecked")
		V result = (V) cache.get(object, key);
		@SuppressWarnings("unchecked")
		V cacheMiss = (V) cacheMisses.get(key);

		if (result == null) {
			result = computer.apply(object);
			if (result == null) {
				result = cacheMiss;
			}
			cache.put(object, key, result);
		}

		if (result == cacheMiss) {
			@SuppressWarnings("unchecked")
			V defaultValue = (V) defaults.get(key);
			result = defaultValue;
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	public <T extends EObject> EList<T> getReferencers(EObject object, EReference reference) {
		return (EList<T>) getCacheAdapter(object).getInverseReferences(object).stream()
				.filter(setting -> setting.getEStructuralFeature() == reference)
				.map(EStructuralFeature.Setting::getEObject)
				.collect(Collectors.toCollection(UniqueEList.FastCompare::new));
	}

	public Collection<EStructuralFeature.Setting> getInverseReferences(EObject object, EReference reference) {
		return getCacheAdapter(object).getInverseReferences(object).stream()
				.filter(setting -> setting.getEStructuralFeature() == reference)
				.collect(Collectors.toCollection(UniqueEList.FastCompare::new));
	}

	public Collection<EStructuralFeature.Setting> getInverseReferences(EObject object) {
		return getCacheAdapter(object).getInverseReferences(object);
	}

}
